#include "EncryptionManager.h"
#include "PKEncrypter.h"
#include "SymmetricEncrypter.h"
#include "SymmetricKeyMemory.h"
#include "SymmetricKeyFile.h"
#include "KeyGenerator.h"
#include "CryptoMetadata.h"

#include <assert.h>
#include <memory>
#include <QCryptographicHash>

namespace kryptos { namespace cryptography {

EncryptionManager::EncryptionManager()
{
}

EncryptionManager::~EncryptionManager()
{

}

//TODO: clase base CryptoMsg con la logica basica para guardar receptores, claves de sesion siguiendo la estructura acorde al tipo
//      Heredar para tener 2 variantes, CryptoFile y CryptoBuffer, cryptobuffer sirve para la encriptacion de selecciones de texto
//      y tiene la opcion de codificar los datos en base64 o base26, agregando headers tipo OpenPGP
//      idealmente deberian ser ofstreams para poder usarlos en los filtros de Botan y para que los metodos que escriban los datos
//      encriptados se abstraigan de tipo de destino, y manejen una clase generica CryptoMsg
int EncryptionManager::encryptWithPublicKey(const std::string& file, const std::string& output, std::vector<PublicKey> recipients)
{
    CryptoFile cryptoFile(output, CryptoFile::K_PK01);

    std::auto_ptr<SymmetricKey> sessionKey(KeyGenerator::generateSessionKey(32));
    std::vector<char> encryptedSessionKey;

    foreach(const PublicKey& pk, recipients)
    {
        encryptedSessionKey.clear();
        PKEncrypter pkEncrypter(pk);

        pkEncrypter.encryptBuffer(sessionKey->nextBytes(32).data(), 32, encryptedSessionKey);

        cryptoFile.addRecipient(CryptoRecipient(pk.info().keyid(), encryptedSessionKey.data(), encryptedSessionKey.size()));
        sessionKey->goBack(32);
    }

    SymmetricEncrypter symEncrypter(*sessionKey);
    symEncrypter.encryptFile(file, cryptoFile.outputStream());

    // TODO: firmar archivo con clave privada del emisor

    return 0;
}

int EncryptionManager::encryptWithHybridFormat(const std::string& file, const std::string& output, std::vector<PublicKey> recipients, SymmetricKey& symkey)
{
    CryptoFile cryptoFile(output, CryptoFile::K_OTPRSA01);

    std::auto_ptr<SymmetricKey> sessionKey(KeyGenerator::generateSessionKey(32));
    std::vector<char> encryptedSessionKey;

    foreach(const PublicKey& pk, recipients)
    {
        encryptedSessionKey.clear();
        PKEncrypter pkEncrypter(pk);

        pkEncrypter.encryptBuffer(sessionKey->nextBytes(32).data(), 32, encryptedSessionKey);

        cryptoFile.addRecipient(CryptoRecipient(pk.info().keyid(), encryptedSessionKey.data(), encryptedSessionKey.size()));
        sessionKey->goBack(32);
    }

    CryptoMetadata metadata;
    metadata.addSymmetricKeyInfo(symkey.id(), symkey.offset());
    metadata.encrypt(*sessionKey);

    SymmetricEncrypter symEncrypter(symkey);
    symEncrypter.encryptFile(file, cryptoFile.outputStream());

    cryptoFile.setMetadata(metadata);

    return 0;
}

int EncryptionManager::decrypt(const std::string& file, const std::string& output, const PrivateKey& privateKey, SymmetricKey& key)
{
    int result = 0;

    CryptoFile cryptoFile(file);
    unsigned long long size = cryptoFile.size();

    switch (cryptoFile.format())
    {
    case CryptoFile::K_PK01:     result = decryptPKCryptoFile(cryptoFile, output, privateKey); break;
    case CryptoFile::K_OTPRSA01: result = decryptHybridCryptoFile(cryptoFile, output, privateKey, key); break;
    }

    return result;
}

std::vector<CryptoRecipient> EncryptionManager::cryptoRecipients(const std::string& encryptedFile)
{
    CryptoFile cryptoFile(encryptedFile);
    return cryptoFile.recipients();
}

CryptoFile::CryptoFormat EncryptionManager::cryptoFormat(const std::string& encryptedFile)
{
    CryptoFile cryptoFile(encryptedFile);
    return cryptoFile.format();
}

std::string EncryptionManager::symmetricKeyNeeded(const std::string& encryptedFile, const PrivateKey& privateKey)
{
    std::string symkeyId;

    CryptoFile cryptoFile(encryptedFile);

    std::vector<char> decodedSessionKey;

    std::vector<CryptoRecipient> recipients = cryptoFile.recipients();
    foreach (const CryptoRecipient& recipient, recipients)
    {
        if (recipient.id() == privateKey.info().keyid())
        {
            PKEncrypter pkDecrypter(privateKey);
            pkDecrypter.decryptBuffer(recipient.sessionKey().data(), recipient.sessionKey().size(), decodedSessionKey);
            break;
        }
    }

    if (!decodedSessionKey.empty())
    {
        SymmetricKeyMemory sessionKey(decodedSessionKey.data(), decodedSessionKey.size(), true);

        CryptoMetadata metadata = cryptoFile.metadata();
        metadata.decrypt(sessionKey);

        std::pair<std::string, unsigned long long> firstkey = metadata.keys().front();
        symkeyId = firstkey.first;
    }

    return symkeyId;
}

unsigned long long EncryptionManager::symmetricKeyLengthNeededToDecrypt(const std::string& encryptedFile)
{
    CryptoFile cryptoFile(encryptedFile);

    return cryptoFile.format() == CryptoFile::K_OTPRSA01
                            ? (cryptoFile.cryptoDataEnd() - cryptoFile.cryptoDataOffset())
                            : 0;
}

bool EncryptionManager::isValidCryptoFile(const std::string encryptedFile)
{
    assert(false);
    return true;
}

int EncryptionManager::decryptPKCryptoFile(CryptoFile& cryptoFile, const std::string& output, const PrivateKey& privateKey)
{
    int result = 0;
    std::vector<char> decodedSessionKey;

    std::vector<CryptoRecipient> recipients = cryptoFile.recipients();
    foreach (const CryptoRecipient& recipient, recipients)
    {
        if (recipient.id() == privateKey.info().keyid())
        {
            PKEncrypter pkDecrypter(privateKey);
            pkDecrypter.decryptBuffer(recipient.sessionKey().data(), recipient.sessionKey().size(), decodedSessionKey);
            break;
        }
    }

    if (!decodedSessionKey.empty())
    {
        SymmetricKeyMemory symkey(decodedSessionKey.data(), decodedSessionKey.size(), true);
        SymmetricEncrypter decrypter(symkey);

        std::istream& encfile = cryptoFile.inputStream();
        encfile.seekg(cryptoFile.cryptoDataOffset());
        std::ofstream decfile(output.c_str(), std::ios::binary);

        result = decrypter.decryptFile(encfile, decfile);
        decfile.flush();
        decfile.close();
    }
    else
    {
        result = -1;
    }

    return result;
}

//TODO: tiene que recibir un objeto que mapee los ids de claves simetricas con su correspondiente clave simetrica
int EncryptionManager::decryptHybridCryptoFile(CryptoFile& cryptoFile, const std::string& output, const PrivateKey& privateKey, SymmetricKey& key)
{
    int result = 0;

    std::vector<char> decodedSessionKey;

    std::vector<CryptoRecipient> recipients = cryptoFile.recipients();
    foreach (const CryptoRecipient& recipient, recipients)
    {
        if (recipient.id() == privateKey.info().keyid())
        {
            PKEncrypter pkDecrypter(privateKey);
            pkDecrypter.decryptBuffer(recipient.sessionKey().data(), recipient.sessionKey().size(), decodedSessionKey);
            break;
        }
    }

    if (!decodedSessionKey.empty())
    {
        SymmetricKeyMemory symkey(decodedSessionKey.data(), decodedSessionKey.size(), true);
        CryptoMetadata metadata = cryptoFile.metadata();
        metadata.decrypt(symkey);

        std::pair<std::string, unsigned long long> firstkey = metadata.keys().front();
        unsigned long long offset = firstkey.second;

        //SymmetricKeyFile symfile(firstkey.first);
        key.setOffset(offset);

        SymmetricEncrypter decrypter(key);

        std::istream& encfile = cryptoFile.inputStream();
        encfile.seekg(cryptoFile.cryptoDataOffset());
        std::ofstream decfile(output.c_str(), std::ios::binary);

        result = decrypter.decryptFile(encfile, cryptoFile.cryptoDataEnd(), decfile);
        decfile.flush();
        decfile.close();
    }
    else
    {
        result = -1;
    }

    return result;
}

}}
